package com.googlecode.mvcguice.routing;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.lang.reflect.InvocationTargetException;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;

import com.google.common.primitives.Primitives;
import com.googlecode.mvcguice.RequestContext;
import com.googlecode.mvcguice.util.Types;

public abstract class ParameterBinder {
	protected final Class<?> type;

	private ParameterBinder(Class<?> type) {
		this.type = type;
	}

	public abstract Object bind(RequestContext requestContext);

	public abstract String getName();

	public final Class<?> getType() {
		return type;
	}

	public static ParameterBinder forConstant(Class<?> type, Object value) {
		checkNotNull(type, "Parameter 'type' must be not null");
		checkNotNull(value, "Parameter 'value' must be not null");
		return new ConstantParameterBinder(type, value);
	}

	public static ParameterBinder forVariable(Class<?> type, String name) {
		checkNotNull(type, "Parameter 'type' must be not null");
		checkNotNull(name, "Parameter 'name' must be not null");

		Converter converter = ConvertUtils.lookup(type);
		checkArgument(converter != null, "Unsupported binding parameter type: " + type);

		return new VariableParameterBinder(type, converter, name);
	}

	//TODO: add arrays of primitives support
	public static ParameterBinder forSimpleParam(Class<?> type, String name) {
		return forSimpleParam(type, name, null);
	}

	public static ParameterBinder forSimpleParam(Class<?> type, String name, Object defaultValue) {
		checkNotNull(type, "Parameter 'type' must be not null");
		checkNotNull(name, "Parameter 'name' must be not null");
		checkArgument(Types.isSimpleType(type), "Type must be simple");

		Converter converter = ConvertUtils.lookup(type);
		checkArgument(converter != null, "Unsupported binding parameter type: " + type);

		return new SimpleParamParameterBinder(type, converter, name, defaultValue);
	}

	public static ParameterBinder forComplexParam(Class<?> type, String name) {
		checkNotNull(type, "Parameter 'type' must be not null");
		checkNotNull(name, "Parameter 'name' must be not null");
		checkArgument(!Types.isSimpleType(type), "Type must be not simple");

		return new ComplexParamParameterBinder(type, name);
	}

	public static ParameterBinder forRequest() {
		return new RequestParameterBinder();
	}

	public static ParameterBinder forResponse() {
		return new ResponseParameterBinder();
	}

	private static class ConstantParameterBinder extends ParameterBinder {
		private final Object value;

		ConstantParameterBinder(Class<?> type, Object value) {
			super(type);
			this.value = value;
		}

		@Override
		public Object bind(RequestContext requestContext) {
			return value;
		}

		@Override
		public String getName() {
			return null;
		}

		@Override
		public String toString() {
			return "const(" + value + ")";
		}
	}

	// TODO: make converters plugable
	private static class VariableParameterBinder extends ParameterBinder {
		private final Converter converter;
		private final String name;

		VariableParameterBinder(Class<?> type, Converter converter, String name) {
			super(type);
			this.converter = converter;
			this.name = name;
		}

		@Override
		public Object bind(RequestContext requestContext) {
			//TODO: investigate consequences of target type null (e.g. default should be applied)
			return converter.convert(null, requestContext.getVariables().get(name));
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public String toString() {
			return Primitives.unwrap(type) + "Var(" + name + ")";
		}
	}

	// TODO: make converters plugable
	private static class SimpleParamParameterBinder extends ParameterBinder {
		private final Converter converter;
		private final String name;
		private final Object defaultValue;

		SimpleParamParameterBinder(Class<?> type, Converter converter, String name, Object defaultValue) {
			super(type);
			this.converter = converter;
			this.name = name;
			this.defaultValue = defaultValue;
		}

		@Override
		public Object bind(RequestContext requestContext) {
			final String[] values = requestContext.getParams().get(name);
			if (values == null) {
				if (defaultValue == null) {
					// TODO: replace exception
					throw new RuntimeException("Param '" + name + "' is not available");
				} else {
					return defaultValue;
				}
			}

			// TODO: case when type is not array but values has > 1 elements
			final Object value = type.isArray() ? values : values[0];

			//TODO: investigate consequences of target type null (e.g. default should be applied)
			return converter.convert(null, value);
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public String toString() {
			return Primitives.unwrap(type) + "Param(" + name + ")";
		}
	}

	// TODO: make converters plugable
	private static class ComplexParamParameterBinder extends ParameterBinder {
		private final String name;

		ComplexParamParameterBinder(Class<?> type, String name) {
			super(type);
			this.name = name;
		}

		// TODO: rewrite all this code, since current is not efficient and it's only POC
		// May be it could reuse SimpleParameterBinder
		@Override
		public Object bind(RequestContext requestContext) {
			final Object bean;
			try {
				bean = type.newInstance();
			} catch (InstantiationException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}

			final String paramPrefix = name + '.';
			for (Entry<String, String[]> entry : requestContext.getParams().entrySet()) {
				final String paramName = entry.getKey();
				if (!paramName.startsWith(paramPrefix)) {
					continue;
				}

				try {
					BeanUtils.setProperty(bean, paramName.substring(paramPrefix.length()), entry.getValue());
				} catch (IllegalAccessException e) {
					throw new RuntimeException(e);
				} catch (InvocationTargetException e) {
					throw new RuntimeException(e);
				}
			}

			return bean;
		}

		@Override
		public String getName() {
			return null;
		}

		@Override
		public String toString() {
			return "param(" + type.getName() + ".class, " + name + ")";
		}
	}

	private static class RequestParameterBinder extends ParameterBinder {
		RequestParameterBinder() {
			super(HttpServletRequest.class);
		}

		@Override
		public Object bind(RequestContext requestContext) {
			return requestContext.getRequest();
		}

		@Override
		public String getName() {
			return null;
		}

		@Override
		public String toString() {
			return "request()";
		}
	}

	private static class ResponseParameterBinder extends ParameterBinder {
		ResponseParameterBinder() {
			super(HttpServletResponse.class);
		}

		@Override
		public Object bind(RequestContext requestContext) {
			return requestContext.getResponse();
		}

		@Override
		public String getName() {
			return null;
		}

		@Override
		public String toString() {
			return "response()";
		}
	}
}
